﻿#pragma once
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "winsock2.h"
#include <stdio.h>
#include <iostream>
#include <string>
#include <list>
using namespace std;

#pragma comment(lib,"ws2_32.lib")
void ResponseMessage(SOCKET S,string resp, char* FullPath, char* SuffixName);//产生响应报文

//当chrome浏览器打开一个网页时，除了请求网页本身外，还会发出另外一个请求 /favicon.ico，请求谷歌浏览器的图标
//因此Server实际上打开了多个会话Socket，需要一个List来保存这些会话Socket
//保存打开的当前的会话socket

list<SOCKET> sessionSockets;

void main() {
	WSADATA wsaData;
	/*
		select()机制中提供的fd_set的数据结构，实际上是long类型的数组，
		每一个数组元素都能与一打开的文件句柄（不管是socket句柄，还是其他文件或命名管道或设备句柄）建立联系，建立联系的工作由程序员完成.
		当调用select()时，由内核根据IO状态修改fd_set的内容，由此来通知执行了select()的进程哪个socket或文件句柄发生了可读或可写事件。
	*/
	fd_set rfds;				//用于检查socket是否有数据到来的的文件描述符，用于socket非阻塞模式下等待网络事件通知（有数据到来）
	fd_set wfds;				//用于检查socket是否可以发送的文件描述符，用于socket非阻塞模式下等待网络事件通知（可以发送数据）
	bool first_connetion = true;

	int nRc = WSAStartup(0x0202, &wsaData);//初始化Winsock

	if (nRc) {
		printf("Winsock  startup failed with error!\n");
		WSAGetLastError();/*返回调用线程发生的最后一个错误。当特定的 Windows 套接字函数指示发生了错误时，
						  应立即调用此函数以检索失败函数调用的扩展错误代码。*/
	}
	else if (wsaData.wVersion != 0x0202) {
		printf("Winsock version is not correct!\n");
		WSAGetLastError();
	}
	else {
		printf("Winsock  startup Ok!\n");
	}

	//监听socket
	SOCKET srvSocket;

	//服务器地址和client端地址
	sockaddr_in addr, clientAddr;

	//会话socket，负责和client进程通信
	SOCKET sessionSocket;

	//ip地址长度
	int addrLen;

	//创建监听socket
	srvSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (srvSocket != INVALID_SOCKET)
		printf("Socket create Ok!\n");
	else {
		printf("Socket create failed with error!\n");
		WSAGetLastError();
	}

	//自行配置 Web 服务器的监听地址、监听端口和主目录
	printf("-------------------------------------\n");
	char srvIP[20];
	int srvPort;
	char HomeDireAddr[150], FullPath[150];//完整路径由主目录和请求文件组成
	printf("请输入监听IP地址：");
	scanf("%s", srvIP);
	printf("请输入监听端口号："); 
	scanf("%d", &srvPort);
	printf("请输入主目录：");
	scanf("%s", HomeDireAddr);
	printf("-------------------------------------\n");
	//设置服务器的端口和地址
	addr.sin_family = AF_INET;
	addr.sin_port = htons(srvPort);
	addr.sin_addr.S_un.S_addr = inet_addr(srvIP); //主机上任意一块网卡的IP地址


	//绑定
	int rtn = bind(srvSocket, (LPSOCKADDR)&addr, sizeof(addr));
	if (rtn != SOCKET_ERROR)
		printf("Socket bind Ok!\n");
	else {
		printf("Socket bind failed!\n");
		WSAGetLastError();
	}

	//监听
	rtn = listen(srvSocket, 5);
	if (rtn != SOCKET_ERROR)
		printf("Socket listen Ok!\n");
	else {
		printf("Socket listen failed!\n");
		WSAGetLastError();
	}

	//client端地址
	clientAddr.sin_family = AF_INET;//指定协议
	addrLen = sizeof(clientAddr);

	//设置接收缓冲区
	char recvBuf[4096];

	u_long blockMode = 1;//将srvSock设为非阻塞模式以监听client连接请求

	//调用ioctlsocket，将srvSocket改为非阻塞模式，改成反复检查fd_set元素的状态，看每个元素对应的句柄是否可读或可写
	if ((rtn = ioctlsocket(srvSocket, FIONBIO, &blockMode) == SOCKET_ERROR)) { //FIONBIO：允许或禁止套接口s的非阻塞模式。
		cout << "ioctlsocket() failed with error!\n";
		return;
	}
	cout << "ioctlsocket() for server socket ok!	Waiting for client connection\n";

	while (true) {
		//清空rfds和wfds数组
		FD_ZERO(&rfds);
		FD_ZERO(&wfds);

		//将srvSocket加入rfds数组
		//即：当client端连接请求到来时，rfds数组里srvSocket对应的的状态为可读
		//因此这条语句的作用就是：设置等待client连接请求
		FD_SET(srvSocket, &rfds);
		for (list<SOCKET>::iterator itor = sessionSockets.begin(); itor != sessionSockets.end(); itor++) {
			//将sessionSockets里的每个会话socket加入rfds数组和wfds数组
			//即：当client端发送数据过来时，rfds数组里sessionSocket的对应的状态为可读；当可以发送数据到client端时，wfds数组里sessionSocket的对应的状态为可写
			//因此下面二条语句的作用就是：
			//设置等待所有会话SOKCET可接受数据或可发送数据
			FD_SET(*itor, &rfds);
			FD_SET(*itor, &wfds);
		}
		/*
			select工作原理：传入要监听的文件描述符集合（可读、可写，有异常）开始监听，select处于阻塞状态。
			当有可读写事件发生或设置的等待时间timeout到了就会返回，返回之前自动去除集合中无事件发生的文件描述符，返回时传出有事件发生的文件描述符集合。
			但select传出的集合并没有告诉用户集合中包括哪几个就绪的文件描述符，需要用户后续进行遍历操作(通过FD_ISSET检查每个句柄的状态)。
		*/
		//开始等待，等待rfds里是否有输入事件，wfds里是否有可写事件
		//The select function returns the total number of socket handles that are ready and contained in the fd_set structure
		//返回总共可以读或写的句柄个数
		
		int nTotal = select(0, &rfds, &wfds, NULL, NULL);

		//如果srvSock收到连接请求，接受client连接请求
		if (FD_ISSET(srvSocket, &rfds)) {
			nTotal--;   //因为client端请求到来也算可读事件，因此-1，剩下的就是真正有可读事件的句柄个数（即有多少个socket收到了数据）
			//产生新的会话SOCKET
			sessionSocket = accept(srvSocket, (LPSOCKADDR)&clientAddr, &addrLen);
			if (sessionSocket != INVALID_SOCKET)
				printf("Socket listen one client request!\n");
			else {
				printf("Socket与client连接失败！");
				WSAGetLastError();
			}
			printf("client端IP地址：%s\n", inet_ntoa(clientAddr.sin_addr));
			printf("client端端口号：%u\n", htons(clientAddr.sin_port));
			//把会话SOCKET设为非阻塞模式
			if ((rtn = ioctlsocket(sessionSocket, FIONBIO, &blockMode) == SOCKET_ERROR)) { //FIONBIO：允许或禁止套接口s的非阻塞模式。
				cout << "ioctlsocket() failed with error!\n";
				return;
			}
			cout << "ioctlsocket() for session socket ok!	Waiting for client data\n\n";

			//设置等待会话SOKCET可接受数据或可发送数据
			FD_SET(sessionSocket, &rfds);
			FD_SET(sessionSocket, &wfds);

			//把新的会话socket加入会话socket列表
			sessionSockets.push_back(sessionSocket);
			first_connetion = false;//已经产生了会话socket，因此设为false
		}

		//检查会话SOCKET是否有数据到来
		if (nTotal > 0) { //如果还有可读事件，说明是会话socket有数据到来
			//遍历所有会话socket，如果会话socket有数据到来，则接受client的数据
			for (list<SOCKET>::iterator itor = sessionSockets.begin(); itor != sessionSockets.end(); itor++) {
				if (FD_ISSET(*itor, &rfds)) { //如果遍历到的当前socket有数据到来
					//receiving data from client
					memset(recvBuf, '\0', 4096);
					rtn = recv(*itor, recvBuf, 4096, 0);
					if (rtn > 0) {
						printf("Received %d bytes from client: \n%s\n", rtn, recvBuf);

						//从请求报文中获取请求的文件名和完整路径 
						//一个典型的HTTP请求报文
						//GET /somedir/page.html HTTP/1.1
						//Host: www.someschool.edu
						//Connection: close
						//User-agent: Mozilla/5.0
						//Accept-language: fr
						char RequestFileName[50]="";//请求文件名
						char SuffixName[10]="";//后缀
						for (int i = 0; i < rtn; i++) {
							if (recvBuf[i] == '/') { //从第一个/开始到第一个空格就是文件名
								for (int k = 0; k < rtn; k++) {
									if (recvBuf[i] != ' ') {
										RequestFileName[k] = recvBuf[i];
										i++;
									}
									else {
										RequestFileName[k + 1] = '\0';
										break;
									}
								}
								break;
							}
						}
						for (int i = 0; i < rtn; i++) {
							if (recvBuf[i] == '.') { //从第一个.开始到第一个空格就是后缀名，注意不要把.存进去了
								for (int k = 0; k < rtn; k++) {
									if (recvBuf[i + 1] != ' ') {
										SuffixName[k] = recvBuf[i + 1];
										i++;
									}
									else {
										SuffixName[k + 1] = '\0';
										break;
									}
								}
								break;
							}
						}
						printf("---------------------------------\n");
						strcpy(FullPath, HomeDireAddr);
						strcat(FullPath, RequestFileName);
						if (strcmp(RequestFileName, "/favicon.ico") == 0) {
							continue;
						}
						printf("请求文件名：%s\n", RequestFileName);
						printf("完整路径：%s\n", FullPath);
						printf("---------------------------------\n");

						//发送回client	
						std::string resp;
						if (strcmp(RequestFileName, "/") == 0) {//如输入127.0.0.1:5050，无后缀文件
							std::string content = "Welcome to Lk_Server";
							resp.append("HTTP/1.1 200 OK\r\n");
							resp.append("Server: Lk_Server\r\n");
							resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");				
							resp.append("Content-Length: ").append(std::to_string(content.length())).append("\r\n");
							resp.append("\r\n");
							resp.append(content);
							rtn = send(*itor, resp.c_str(), resp.length(), 0);
							if (rtn > 0) {
								printf("Send %d bytes to client: %s\n", rtn, resp.c_str());
							}
						}
						else ResponseMessage(*itor,resp, FullPath, SuffixName);//产生响应报文
					}
				}				
			}
		}
	}
	closesocket(srvSocket);
	WSACleanup();
	return;
}
void ResponseMessage(SOCKET S, string resp, char* FullPath, char* SuffixName)
{
	char content_Type[20] = "text/html"; //设置content-type
	if (strcmp(SuffixName, "html") == 0) strcpy(content_Type, "text/html");
	if (strcmp(SuffixName, "gif") == 0) strcpy(content_Type, "image/gif");
	if (strcmp(SuffixName, "jpg") == 0) strcpy(content_Type, "image/jpg");
	if (strcmp(SuffixName, "jpeg") == 0) strcpy(content_Type, "image/jpeg");
	if (strcmp(SuffixName, "png") == 0) strcpy(content_Type, "image/png");
	if (strcmp(SuffixName, "mp3") == 0) strcpy(content_Type, "audio/mp3");
	if (strcmp(SuffixName, "mp4") == 0) strcpy(content_Type, "video/mpeg4");

	FILE* fp = fopen(FullPath, "rb");
	int dataLen = 0;
	char* p = nullptr;
	if (fp == NULL) {
		resp.append("HTTP/1.1 404 Not Found\r\n");
		std::string content = "404 Not Found";
		resp.append("Server: Lk_Server\r\n");
		resp.append("Content-Type: text/html; charset=ISO-8859-1\r\n");
		resp.append("Content-Length: ").append(std::to_string(content.length())).append("\r\n");
		resp.append("\r\n");
		resp.append(content);
		int LengthBao = send(S, resp.c_str(), resp.length(), 0);
		if (LengthBao > 0) {
			printf("Send %d bytes to client: %s\n", LengthBao, resp.c_str());
		}
	}
	else {
		resp.append("HTTP/1.1 200 OK\r\n");
		fseek(fp, 0L, SEEK_END);//将文件指针 fp 移动到文件的末尾。
		dataLen = ftell(fp);  //获取当前文件指针位置，返回文件的字节数
		p = (char*)malloc(dataLen + 1);//
		fseek(fp, 0L, SEEK_SET);//将文件指针fp重置回文件的开始位置，从头开始读取文件内容。
		fread(p, dataLen, 1, fp);
		resp.append("Server: Lk_Server\r\n");
		resp.append("Content-Type: ").append(content_Type).append("; charset = ISO - 8859 - 1\r\n");
		resp.append("Content-Length: ").append(std::to_string(dataLen)).append("\r\n");
		resp.append("\r\n");
	}
	int LengthBao = send(S, resp.c_str(), resp.length(), 0);
	int rtn = send(S, p, dataLen, 0);
	if (rtn + LengthBao > 0) {
		printf("Send %d bytes to client: %s\n", rtn + LengthBao, resp.c_str());
	}
}
